﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using Apache.Cassandra;

namespace Cassandraemon.Serialization
{
    public abstract class CassandraSerializer
    {
        #region Static Instances

        public static CassandraSerializer GetSerializer(Type type)
        {
            return CassandraSerializerFactory.Current.GetSerializer(type);
        }

        public static CassandraSerializer<T> GetSerializer<T>()
        {
            var value = CassandraSerializerFactory.Current.GetSerializer(typeof(T));
            return (CassandraSerializer<T>)value;
        }

        #endregion

        #region High-Level APIs

        public static List<Column> ToColumns(object obj)
        {
            var type = obj.GetType();
            var serializer = GetSerializer(type);
            var columns = new List<Column>();
            serializer.ToColumnsCore(obj, columns);
            return columns;
        }

        public static T ToObject<T>(IEnumerable<Column> columns)
        {
            var serializer = GetSerializer(typeof(T));
            var typed = serializer as CassandraSerializer<T>;
            var obj = typed != null ?
                typed.CreateInstance() :
                Activator.CreateInstance<T>();
            serializer.ToObjectCore(columns, obj);
            return obj;
        }

        #endregion

        #region Core Serializations

        protected abstract void ToColumnsCore(object obj, ICollection<Column> columns);
        protected abstract void ToObjectCore(IEnumerable<Column> columns, object obj);

        #endregion
    }

    public abstract class CassandraSerializer<T>
        : CassandraSerializer
    {
        public virtual T CreateInstance()
        {
            return Activator.CreateInstance<T>();
        }
    }

    public abstract class CassandraSerializerWithNew<T>
        : CassandraSerializer<T>
        where T: new()
    {
        public sealed override T CreateInstance()
        {
            return new T();
        }
    }

    public abstract class CassandraSerializerFactory
    {
        #region Constructors/Static Instances

        private static CassandraSerializerFactory _Current =
            CompiledCassandraSerializerFactory.Default;

        public static CassandraSerializerFactory Current
        {
            get { return _Current ?? CompiledCassandraSerializerFactory.Default; }
            set { _Current = value; }
        }

        #endregion

        public abstract CassandraSerializer GetSerializer(Type type);
    }

    public abstract class CachedCassandraSerializerFactory
        : CassandraSerializerFactory
    {
        readonly ConcurrentDictionary<Type, CassandraSerializer> _SerializerByType =
            new ConcurrentDictionary<Type, CassandraSerializer>();

        public override CassandraSerializer GetSerializer(Type type)
        {
            var serializerByType = _SerializerByType;
            return serializerByType.GetOrAdd(type, CreateSerializer);
        }

        public void SetSerializer(Type type, CassandraSerializer value)
        {
            var serializerByType = _SerializerByType;
            if (value == null)
            {
                serializerByType.TryRemove(type, out value);
            }
            else
            {
                serializerByType[type] = value;
            }
        }

        protected abstract CassandraSerializer CreateSerializer(Type type);
    }

    public sealed class CompiledCassandraSerializerFactory
        : CachedCassandraSerializerFactory
    {
        #region Constructors & Static Instances

        /// <summary>
        /// We do not support mulitple instances of this class as of today,
        /// due to the lack of testing for generating multiple dynamic assemblies.
        /// </summary>
        CompiledCassandraSerializerFactory()
        {
        }

        static readonly CompiledCassandraSerializerFactory _Default =
            new CompiledCassandraSerializerFactory();

        public static CompiledCassandraSerializerFactory Default
        {
            get { return _Default; }
        }

        #endregion

        #region CreateSerializer

        protected override CassandraSerializer CreateSerializer(Type type)
        {
            PropertyInfo[] properties = type.GetProperties();
            return this.CreateSerializer(type, properties);
        }

        public CassandraSerializer CreateSerializer(
            Type type,
            IEnumerable<PropertyInfo> properties)
        {
            return this.CreateSerializer(type,
                properties.Select(p => Tuple.Create(p, (byte[])null)));
        }

        #endregion

        #region Dynamic Class Generation

        class SerializePropertyInfo
        {
            public SerializePropertyInfo(Tuple<PropertyInfo, byte[]> tuple)
            {
                var property = tuple.Item1;
                this.PropertyName = property.Name;
                this.PropertyType = property.PropertyType;
                this.ColumnName = tuple.Item2 ?? property.Name.ToCassandraByte();
                this.Getter = property.GetGetMethod();
                this.Setter = property.GetSetMethod(true);
            }

            public readonly string PropertyName;
            public readonly Type PropertyType;
            public readonly byte[] ColumnName;
            public readonly MethodInfo Getter;
            public readonly MethodInfo Setter;
            internal FieldBuilder ColumnNameFieldBuilder;
            internal FieldBuilder ColumnNameInitializedData;
        }

        public CassandraSerializer CreateSerializer(
            Type type,
            IEnumerable<Tuple<PropertyInfo, byte[]>> propertyInfoAndColumnNames)
        {
            var properties = propertyInfoAndColumnNames
                .Select(t => new SerializePropertyInfo(t))
                .Where(p =>
                    p.Getter != null &&
                    p.Setter != null)
                .ToList();

            // Get the super class

            var baseClass = typeof(DynamicCassandraSerializer<>)
                .MakeGenericType(type);

            // Define a new class, derived from the super class

            var mb = this.GetModuleBuilder();
            var typeFullName = string.Concat(
                baseClass.Namespace,
                ".CassandraSerializer_",
                type.FullName.Replace('.', '_'));
            var tb = mb.DefineType(typeFullName,
                TypeAttributes.Public | TypeAttributes.Sealed,
                baseClass);

            // Define static fileds that holds ColumnName

            foreach (var p in properties)
            {
                var fb = tb.DefineField(
                    string.Concat("_", p.PropertyName),
                    typeof(byte[]),
                    FieldAttributes.Static | FieldAttributes.InitOnly);
                var id = tb.DefineInitializedData(
                    fb.Name,
                    p.ColumnName,
                    FieldAttributes.Static);
                p.ColumnNameFieldBuilder = fb;
                p.ColumnNameInitializedData = id;
            }

            // Define TypeInitializer to initialize static fields

            var cctor = tb.DefineTypeInitializer();
            var il = cctor.GetILGenerator();
            var initArray = typeof(RuntimeHelpers).GetMethod("InitializeArray");
            foreach (var p in properties)
            {
                var data = p.ColumnName;
                il.Emit(OpCodes.Ldc_I4, data.Length);
                il.Emit(OpCodes.Newarr, typeof(byte));
                il.Emit(OpCodes.Dup);
                il.Emit(OpCodes.Ldtoken, p.ColumnNameInitializedData);
                il.Emit(OpCodes.Call, initArray);
                il.Emit(OpCodes.Stsfld, p.ColumnNameFieldBuilder);
            }
            il.Emit(OpCodes.Ret);

            // override ToColumns()

            var columnCtor = typeof(Column).GetConstructor(new Type[0]);
            var columnSetNameMethod = typeof(Column).GetProperty("Name").GetSetMethod();
            var columnSetValueMethod = typeof(Column).GetProperty("Value").GetSetMethod();
            var listAddMethod = typeof(ICollection<Column>).GetMethod("Add");
            var toCassandraByte = typeof(ObjectExtensions).GetMethod("ToCassandraByte", new[] { typeof(object) });

            var method = tb.DefineMethod("ToColumnsCore",
                MethodAttributes.Family | MethodAttributes.Virtual | MethodAttributes.Final,
                CallingConventions.Standard,
                null,
                new Type[] { typeof(object), typeof(ICollection<Column>) });
            il = method.GetILGenerator();
            var locValue = il.DeclareLocal(typeof(object));
            foreach (var p in properties)
            {
                var lblEndAdd = il.DefineLabel();

                // var value = obj.Property;
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Callvirt, p.Getter);
                var propertyType = p.PropertyType;
                if (!propertyType.IsClass)
                {
                    // Box if value type
                    il.Emit(OpCodes.Box, propertyType);
                    il.Emit(OpCodes.Stloc_0);
                }
                else
                {
                    // if (value == null) { goto lblEndAdd; }
                    il.Emit(OpCodes.Dup);
                    il.Emit(OpCodes.Stloc_0);

                    il.Emit(OpCodes.Ldnull);
                    il.Emit(OpCodes.Ceq);
                    il.Emit(OpCodes.Brtrue_S, lblEndAdd);
                }

                // push columns for columns.Add()
                il.Emit(OpCodes.Ldarg_2);

                // new Column()
                il.Emit(OpCodes.Newobj, columnCtor);

                // .Name = p.ColumnName
                il.Emit(OpCodes.Dup); // column
                il.Emit(OpCodes.Ldsfld, p.ColumnNameFieldBuilder);
                il.Emit(OpCodes.Callvirt, columnSetNameMethod);

                // .Value = obj.Property.ToCassandraByte();
                il.Emit(OpCodes.Dup); // column
                il.Emit(OpCodes.Ldloc_0); // value
                il.Emit(OpCodes.Call, toCassandraByte);
                il.Emit(OpCodes.Callvirt, columnSetValueMethod);

                // columns.Add(column);
                il.Emit(OpCodes.Callvirt, listAddMethod);

                il.MarkLabel(lblEndAdd);
            }
            il.Emit(OpCodes.Ret);

            // Create SerializerPropertyInfo for ToObject

            var rgdpi = new List<Tuple<Type, byte[], DynamicMethod>>();
            foreach (var p in properties)
            {
                var dm = new DynamicMethod(
                    "",
                    null,
                    new[] { type, typeof(object) },
                    true);
                il = dm.GetILGenerator();
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                var propertyType = p.PropertyType;
                if (propertyType.IsClass)
                {
                    il.Emit(OpCodes.Castclass, propertyType);
                }
                else
                {
                    il.Emit(OpCodes.Unbox_Any, propertyType);
                }
                il.Emit(OpCodes.Callvirt, p.Setter);
                il.Emit(OpCodes.Ret);

                rgdpi.Add(Tuple.Create(
                    propertyType,
                    p.ColumnName,
                    dm));
            }

            // Define constructor

            var ctorArgTypes = new[] { typeof(IEnumerable<Tuple<Type, byte[], DynamicMethod>>) };
            var baseCtor = baseClass.GetConstructor(
                BindingFlags.NonPublic | BindingFlags.Instance,
                null,
                ctorArgTypes,
                null);
            var ctor = tb.DefineConstructor(
                MethodAttributes.Public,
                CallingConventions.Standard,
                ctorArgTypes);
            il = ctor.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Call, baseCtor);
            il.Emit(OpCodes.Ret);

            // Now, compile and create the class.

            var serializerType = tb.CreateType();
            var serializerInstance = Activator.CreateInstance(serializerType, rgdpi);
            return (CassandraSerializer)serializerInstance;
        }

        /// <summary>
        /// This class is for internal use only.
        /// It is labeled as public because otherwise TypeBuilder.CreateType() will fail,
        /// but external code should not use this class directly as it may change in future.
        /// </summary>
        public abstract class DynamicCassandraSerializer<T>
            : CassandraSerializerWithNew<T>
            where T : new()
        {
            protected DynamicCassandraSerializer(
                IEnumerable<Tuple<Type, byte[], DynamicMethod>> properties)
            {
                this._spiByName = properties
                    .ToDictionary(p => p.Item2,
                        p => new DynamicPropertyInfo
                        {
                            Type = p.Item1,
                            Setter = (Action<T, object>)p.Item3.CreateDelegate(typeof(Action<T, object>)),
                        },
                        SequenceComparer<byte>.Default);
            }

            class DynamicPropertyInfo
            {
                public Type Type;
                public Action<T, object> Setter;
            };

            readonly IDictionary<byte[], DynamicPropertyInfo> _spiByName;

            protected sealed override void ToObjectCore(IEnumerable<Column> columns, object obj)
            {
                var typed = (T)obj;
                var spiByName = _spiByName;
                foreach (var column in columns)
                {
                    DynamicPropertyInfo spi;
                    if (spiByName.TryGetValue(column.Name, out spi))
                    {
                        var value = MappingUtil.MappingValueType(spi.Type, column.Value);
                        spi.Setter(typed, value);
                    }
                }
            }
        }

        #endregion

        #region Assembly/ModuleBuilder

        AssemblyBuilder _AssemblyBuilder;
        ModuleBuilder _ModuleBuilder;

        ModuleBuilder GetModuleBuilder()
        {
            var mb = this._ModuleBuilder;
            if (mb != null)
            {
                return mb;
            }

            var an = new AssemblyName("CasandraSerializer");
            var ab = AppDomain.CurrentDomain.DefineDynamicAssembly(
                an,
                AssemblyBuilderAccess.RunAndSave);
            mb = ab.DefineDynamicModule("MainModule");
            this._AssemblyBuilder = ab;
            this._ModuleBuilder = mb;
            return mb;
        }

        #endregion
    }
}
